package org.terramagnet.mvcframework.resource.service;

import org.terramagnet.mvcframework.resource.Identify;
import org.terramagnet.mvcframework.resource.ResourceArchive;
import org.terramagnet.mvcframework.resource.ResourceCleaner;
import org.terramagnet.mvcframework.resource.ResourceService;
import org.terramagnet.mvcframework.tools.CnToSpell;
import org.terramagnet.mvcframework.tools.Hex62Util;
import org.terramagnet.mvcframework.tools.SpringMvcUtils;
import org.terramagnet.mvcframework.resource.dao.ResourceDao;
import org.terramagnet.mvcframework.resource.thumbnails.DimensionThumbnails;
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.terramagnet.mvcframework.ApplicationRuntimeException;
import org.terramagnet.mvcframework.friendly.I18nFriendly;
import org.terramagnet.mvcframework.tools.Paginal;

/**
 * 本地资源服务器.
 *
 * <p>
 * 推荐使用路径扫描自动配置:</p>
 * <pre><code>&lt;context:component-scan base-package="org.terramagnet.mvcframework.resource"/&gt;</code></pre>
 *
 * @author terrason
 */
@Service
public class LocalResourceService implements ResourceService {

    private static final Pattern schema = Pattern.compile("^(http|https|ftp)://.*");
    private static final Pattern THUMBNAIL_VIEW = Pattern.compile("([\\p{Alnum}/]+)\\-(\\d+)(x|X)(\\d+)(\\.\\p{Alnum}+)?$");
    private static final String SLASH = "/";
    private final DimensionThumbnails dimensionThumbnails = new DimensionThumbnails();
    @Resource
    private ResourceCleaner resourceCleaner;
    @Resource
    private ResourceDao resourceDao;

    @Value("#{config['resource.identify'] ?: 'hex'}")
    private String _identify;//identify|path|hash|hex
    @Value("#{config['resource.root'] ?: null}")
    private String _root;
    @Value("#{config['resource.autoclean.cron' ?: null]}")
    private String cron;

    /**
     * 查看资源内容. <strong>等价于{@link #view(java.lang.String, boolean) view(key,false)}</strong>
     *
     * @param key 资源KEY 支持资源URL和资源ID,支持缩放图
     * @return 资源或{@code null}
     */
    public ResourceArchive view(String key) {
        return view(key, false);
    }

    /**
     * 查看资源内容.
     *
     * @param key 资源KEY 支持资源URL和资源ID,支持缩放图
     * @param thumbnail 是否返回缩略图资源
     * @return 资源或{@code null}
     */
    @Override
    public ResourceArchive view(String key, boolean thumbnail) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        Dimension dimension = null;
        Matcher matcher = THUMBNAIL_VIEW.matcher(key);
        String mainKey = key;
        if (matcher.matches()) {
            int width = Integer.parseInt(matcher.group(2));
            int height = Integer.parseInt(matcher.group(4));
            dimension = new Dimension(width, height);
            String extension = StringUtils.trimToEmpty(matcher.group(5));
            mainKey = matcher.group(1) + extension;
        }
        ResourceArchive resource = keyToResource(mainKey);
        if (resource == null) {
            return null;
        }
        resource.setKey(mainKey);
        if (thumbnail && resource.exists() && resource.isImage() && dimension != null) {
            ResourceArchive thumbnails = dimensionThumbnails.toThumbmails(resource, dimension);
            thumbnails.setId(resource.getId());
            thumbnails.setKey(key);
            return thumbnails;
        }
        return resource;
    }

    /**
     * 创建资源. 创建后的资源是<strong>临时的</strong>。 <strong>临时的</strong>资源会在一段时间后删除。
     *
     * @param bucket 资源桶 资源桶用于对资源进行业务分组.
     * @param multipartFiles 资源文件
     * @return 资源key集合
     */
    @Override
    public List<String> create(String bucket, MultipartFile... multipartFiles) {
        List<String> keys = new ArrayList<String>();
        for (ResourceArchive resource : createResource(bucket, multipartFiles)) {
            keys.add(resource.getKey());
        }
        return keys;
    }

    @Override
    public String create(String bucket, String filename, InputStream inputStream) {
        return create(bucket, new SimpleMultipartFile(filename, inputStream)).get(0);
    }

    /**
     * 创建资源. 创建后的资源是<strong>临时的</strong>。 <strong>临时的</strong>资源会在一段时间后删除。
     *
     * @param bucket 资源桶 资源桶用于对资源进行业务分组.
     * @param multipartFile 资源文件
     * @return 资源
     */
    public ResourceArchive createSingleResource(String bucket, MultipartFile multipartFile) throws IOException {
        if (multipartFile == null) {
            return null;
        }
        return createResource(bucket, new MultipartFile[]{multipartFile}).get(0);
    }

    /**
     * 创建资源. 创建后的资源是<strong>临时的</strong>。 <strong>临时的</strong>资源会在一段时间后删除。
     *
     * @param bucket 资源桶 资源桶用于对资源进行业务分组.
     * @param multipartFiles 资源文件
     * @return 资源集合
     */
    public List<ResourceArchive> createResource(String bucket, MultipartFile[] multipartFiles) {
        if (multipartFiles == null || multipartFiles.length == 0) {
            return null;
        }
        List<ResourceArchive> resources = new ArrayList<ResourceArchive>(multipartFiles.length);

        for (MultipartFile multipartFile : multipartFiles) {
            if (multipartFile == null || multipartFile.isEmpty()) {
                continue;
            }
            String filename = CnToSpell.getFullSpell(multipartFile.getOriginalFilename());
            StringBuilder builder = new StringBuilder(128);
            if (StringUtils.isBlank(bucket)) {
                builder.append(generateDatetimeNamespace());
            } else {
                if (!bucket.startsWith(SLASH)) {
                    builder.append(SLASH);
                }
                builder.append(bucket);
            }
            if (!bucket.endsWith(SLASH)) {
                builder.append(SLASH);
            }
            String key = builder.append(Hex62Util.generateShortUuid())
                .append(SLASH)
                .append(filename)
                .toString();

            ResourceArchive resource = parseResource(key);
            File file = resource.getFile();

            File directory = file.getParentFile();
            if (!directory.exists()) {
                directory.mkdirs();
            }
            try {
                multipartFile.transferTo(file);
            } catch (IOException ex) {
                throw new ApplicationRuntimeException(ex, I18nFriendly.DEFAULT);
            }
            long id = resourceDao.insert(resource);
            resource.setId(id);

            resource.setKey(resourceToKey(resource));
            resources.add(resource);
        }
        return resources;
    }

    /**
     * 连接资源. 连接后的资源才是<strong>正式的</strong>.
     *
     * @param key 资源KEY
     * @param module 扩展参数——所属模块
     * @param principal 扩展参数——所属用户
     */
    @Override
    public void link(String key, String module, String principal) {
        ResourceArchive resource = view(key);
        if (resource == null || !resource.isLocalResource()) {
            throw new ApplicationRuntimeException(new I18nFriendly(404, "resource.notfound"), key);
        }
        if (resource.getId() == null) {
            resourceDao.link(resource.getHash(), module, principal);
        } else {
            resourceDao.link(resource.getId(), module, principal);
        }
    }

    @Transactional
    @Override
    public void delete(String[] keys) {
        ArrayList<ResourceArchive> rses = new ArrayList<ResourceArchive>();
        for (String key : keys) {
            ResourceArchive resource = view(key);
            if (resource != null && resource.isLocalResource()) {
                rses.add(resource);
            }
        }
        delete(rses.toArray(new ResourceArchive[rses.size()]));
    }

    /**
     * 删除资源.
     */
    @Transactional
    public void delete(ResourceArchive... resources) {
        ArrayList<Long> ids = new ArrayList<Long>();
        ArrayList<String> keys = new ArrayList<String>();
        for (ResourceArchive resource : resources) {
            if (resource.getId() == null) {
                keys.add(resource.getHash());
            } else {
                ids.add(resource.getId());
            }
            resourceCleaner.put(resource.getFile());
        }
        if (ids.size() == 1) {
            resourceDao.delete(ids.get(0));
        } else if (ids.size() > 1) {
            resourceDao.batchDelete(ArrayUtils.toPrimitive(ids.toArray(new Long[0])));
        }
        if (keys.size() == 1) {
            resourceDao.delete(keys.get(0));
        } else if (keys.size() > 1) {
            resourceDao.batchDelete(keys.toArray(new String[0]));
        }
    }

    /**
     * 分页查询资源表. 查询条件为{@code null}表示忽略该条件。
     *
     * @param temporary 查询条件——是否临时资源
     * @param from 查询条件——资源的创建起始时间（包含）
     * @param to 查询条件——资源的创建结束时间（不包含）
     * @param page 分页参数——页码
     * @param size 分页参数——大小
     * @return 资源列表. 无记录时返回空列表.
     */
    @Override
    public Paginal<ResourceArchive> page(Boolean temporary, Date from, Date to, int page, int size) {
        Paginal<ResourceArchive> list = resourceDao.selectPaginal(temporary, from, to, page, size);
        for (ResourceArchive resource : list.getElements()) {
            resource.setKey(resourceToKey(resource));
        }
        return list;
    }

    /**
     * 判断资源路径是否是绝对URL.
     */
    @Override
    public boolean isAbsoluteUrl(String path) {
        return schema.matcher(path).matches();
    }

    /**
     * 从绝对地址中截取出可能是本地资源的KEY.
     */
    private PossiblyLocalResource cutPossiblyLocalResourceKeyFormAbsoluteUrl(String absoluteUrl) {
        int form = absoluteUrl.indexOf("://") + 4;
        if (form < 0) {
            return null;
        }

        String contextPath = SpringMvcUtils.currentContextPath();
        int start;
        boolean probable = false;
        if (StringUtils.isBlank(contextPath)) {
            start = absoluteUrl.indexOf(SLASH, form);
            if (start < 0) {
                return null;
            }
        } else {
            start = absoluteUrl.indexOf(contextPath, form);
            if (start < 0) {
                return null;
            }
            start += contextPath.length();
            probable = true;
        }
        String key = absoluteUrl.substring(start);
        if (StringUtils.isBlank(key)) {
            return null;
        }
        return new PossiblyLocalResource(absoluteUrl.substring(start), probable);
    }

    private static class PossiblyLocalResource {

        private final String path;
        /**
         * 很有可能是本地资源.
         */
        private final boolean probable;

        private PossiblyLocalResource(String key, boolean probable) {
            this.path = key;
            this.probable = probable;
        }
    }

    /**
     * 根据资源URL解析出资源. 解析出的资源没有{@link ResourceArchive#getId() id}.
     *
     * @param path 资源URL
     * @return 资源
     */
    private ResourceArchive parseResource(String path) {
        if (isAbsoluteUrl(path)) {
            PossiblyLocalResource possiblyLocalResource = cutPossiblyLocalResourceKeyFormAbsoluteUrl(path);
            if (possiblyLocalResource == null) {
                return new ResourceArchive(null, path);
            } else {
                ResourceArchive resource = parseResource(possiblyLocalResource.path);
                if (resource.exists() || possiblyLocalResource.probable) {
                    return resource;
                }
                return new ResourceArchive(null, path);
            }
        } else {
            StringBuilder filePath = new StringBuilder();
            StringBuilder webPath = new StringBuilder();
            String filePathAndName = path.replace(SLASH, File.separator);

            filePath.append(getRootFilePath());
            if (!filePathAndName.startsWith(File.separator)) {
                filePath.append(File.separatorChar);
                webPath.append(SLASH);
            }
            filePath.append(filePathAndName);
            webPath.append(path);

            File file = new File(filePath.toString());
            File dictory = file.getParentFile();
            if (!dictory.exists()) {
                dictory.mkdirs();
            }
            return new ResourceArchive(file, webPath.toString());
        }
    }

    /**
     * 根据资源KEY值查询或解析出资源. <strong>不关心缩略图逻辑！</strong>
     *
     * @param path 资源KEY值
     * @return 资源
     */
    private ResourceArchive keyToResource(String path) {
        switch (getIdentify()) {
            case IDENTIFY: {
                long id = NumberUtils.toLong(path);
                if (id <= 0) {
                    return null;
                }
                String url = resourceDao.selectPath(id);
                if (url == null) {
                    return null;
                }
                ResourceArchive resource = parseResource(url);
                if (resource == null) {
                    return null;
                }
                resource.setId(id);
                return resource;
            }
            case PATH: {
                return parseResource(path);
            }
            case HASH: {
                if (path.length() != 32) {
                    return null;
                }
                return resourceDao.select(path);
            }
            case HEX: {
                try {
                    long id = Hex62Util.toNumber(path, 62);
                    String p = resourceDao.selectPath(id);
                    if (StringUtils.isBlank(p)) {
                        return null;
                    }
                    ResourceArchive resource = parseResource(p);
                    resource.setId(id);
                    return resource;
                } catch (NumberFormatException ex) {
                    return null;
                }
            }
            default:
                throw new IllegalStateException(getIdentify().toString());
        }
    }

    /**
     * 获取资源的KEY值. <strong>不关心缩略图逻辑！</strong>
     *
     * @param resource 资源
     * @return 资源的KEY值
     */
    private String resourceToKey(ResourceArchive resource) {
        switch (getIdentify()) {
            case IDENTIFY:
                return resource.getId().toString();
            case PATH:
                return resource.getUrl();
            case HASH:
                return resource.getHash();
            case HEX:
                return Hex62Util.toString(resource.getId(), 62);
            default:
                throw new IllegalStateException(getIdentify().toString());
        }
    }
    private String root;

    /**
     * 获取资源服务器文件存放根路径.
     */
    private String getRootFilePath() {
        if (root == null) {
            if (StringUtils.isBlank(_root)) {
                root = SpringMvcUtils.currentServletContext().getRealPath(SLASH);
            } else if (_root.startsWith("../")) {
                File currentApplicationRoot = new File(SpringMvcUtils.currentServletContext().getRealPath(SLASH));
                root = currentApplicationRoot.getParent() + File.separatorChar + _root.substring(3);
            } else {
                root = _root;
            }
        }
        return root;
    }

    /**
     * 获取资源服务器文件存放根目录.
     */
    public File getRootDirectory() {
        return new File(getRootFilePath());
    }

    /**
     * 判断某文件是否能安全管理. 本资源服务器管理的文件目录内的文件才是安全的。
     */
    public boolean isSecurity(File file) {
        String rootFilePath = getRootDirectory().getAbsolutePath();
        return file.getAbsolutePath().startsWith(rootFilePath);
    }

    private String generateDatetimeNamespace() {
        Calendar calendar = Calendar.getInstance();
        return new StringBuilder(11)
            .append(SLASH).append(calendar.get(Calendar.YEAR))
            .append(SLASH).append(calendar.get(Calendar.MONTH))
            .append(SLASH).append(calendar.get(Calendar.DATE))
            .toString();
    }

    private Identify identify;

    private Identify getIdentify() {
        if (identify == null) {
            identify = Identify.valueOf(_identify.toUpperCase());
        }
        if (identify == null) {
            identify = Identify.HEX;
        }
        return identify;
    }

    /**
     * 设置资源的外部标识方式. 共有以下4中方式：identify|path|hash|hex
     * <ul>
     * <li><code>identify</code> -- 直接使用数据库中的资源ID标识</li>
     * <li><code>path</code> -- 资源相对于服务器根目录的资源路径</li>
     * <li><code>hash</code> -- `path`的MD5值</li>
     * <li><code>hex</code> -- <strong>默认</strong> `identify`的62进制值</li>
     * </ul>
     *
     * @param identify 标识方式 identify|path|hash|hex
     */
    public void setIdentify(String identify) {
        this.identify = Identify.valueOf(_identify.toUpperCase());

        if (this.identify == null) {
            this.identify = Identify.HEX;
        }
    }
    @Resource
    private TaskScheduler taskScheduler;

    /**
     * 系统启动时自动执行的代码.
     * <p>
     * <strong>不要在代码中手动调用</strong></p>
     */
    @PostConstruct
    public void startup() {
        if (StringUtils.isNotBlank(cron)) {
            taskScheduler.schedule(new Runnable() {
                @Override
                public void run() {
                    resourceCleaner.deleteTemporaryResource();
                }
            }, new CronTrigger(cron));
        }

    }

    private class SimpleMultipartFile implements MultipartFile {

        private final String filename;
        private final InputStream inputStream;

        public SimpleMultipartFile(String filename, InputStream inputStream) {
            this.filename = filename;
            this.inputStream = inputStream;
        }

        @Override
        public String getName() {
            return filename;
        }

        @Override
        public String getOriginalFilename() {
            return filename;
        }

        @Override
        public String getContentType() {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public long getSize() {
            throw new UnsupportedOperationException();
        }

        @Override
        public byte[] getBytes() throws IOException {
            throw new UnsupportedOperationException();
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return inputStream;
        }

        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            throw new UnsupportedOperationException();
        }

    }

}
